<?php
/**
 * Ruth Library
 * $Id: Field.php 6 2008-01-21 22:03:54Z baron314159@yahoo.com $
 *
 * LICENSE
 *
 * Copyright (c) 2007, Chris Utz
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Impact Lab nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY CHRIS UTZ ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL CHRIS UTZ BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category   Ruth
 * @package    Ruth_Channel
 * @author     Chris Utz <chris@chrisutz.com>
 * @copyright  2007 Chris Utz <chris@chrisutz.com>
 * @license    http://www.opensource.org/licenses/bsd-license.php    New BSD License
 * @version    $Revision: 6 $
 */

/**
 * @see Ruth_Annotation_ReflectionMethod
 */
require_once 'Ruth/Annotation/ReflectionMethod.php';

/**
 * @see Ruth_Channel_Exception
 */
require_once 'Ruth/Channel/Exception.php';

/**
 * @see Ruth_Channel_Validate_Omni
 */
require_once 'Ruth/Channel/Validate/Omni.php';

/**
 * @see Ruth_Filter_Abstract
 */
require_once 'Ruth/Filter/Abstract.php';

/**
 * @see Ruth_Validate_Abstract
 */
require_once 'Ruth/Validate/Abstract.php';


/**
 * Holds information about a field, and allows access to properties/information
 * about a field.
 *
 * @category   Ruth
 * @package    Ruth_Channel
 * @author     Chris Utz <chris@chrisutz.com>
 * @copyright  2007 Chris Utz <chris@chrisutz.com>
 * @license    http://www.opensource.org/licenses/bsd-license.php    New BSD License
 * @version    $Revision: 6 $
 */
class Ruth_Channel_Field
{
    /**
     * Field must be a string.
     */
    const TYPE_STRING = 'string';

    /**
     * Field must be an array.
     */
    const TYPE_ARRAY  = 'array';

    /**
     * isValid will be true if any validator passed.
     */
    const STRATEGY_ANY = 'any';

    /**
     * isValid will be true only if all validators passed.
     */
    const STRATEGY_ALL = 'all';

    /**
     * Used by getNameFiltered to transform the field name to a format that
     * can be shown to an end user.
     *
     * @var Ruth_Filter_Abstract|null
     */
    protected static $_nameFilter = null;

    /**
     * The type of the field. Its value must be one of the TYPE_ constants.
     *
     * @var string
     */
    protected $_type = self::TYPE_STRING;

    /**
     * Array of fields in the Ruth_Channel
     *
     * @var ArrayObject
     */
    protected $_fields;

    /**
     * The name of the field.
     *
     * @var string
     */
    protected $_name = '';

    /**
     * The value of the field.
     *
     * @var string|array
     */
    protected $_value = '';

    /**
     * The filters to apply to the value of this field.
     *
     * @var array
     */
    protected $_filters = array();

    /**
     * The validators this field must pass.
     * 
     * @var array
     */
    protected $_validators = array();

    /**
     * Should a field be valid if any validator passes or must all pass?
     * 
     * @var string
     */
    protected $_validationStrategy = self::STRATEGY_ALL;

    /**
     * If set to a string and the specified validator fails, no subsequent
     * validators are run.
     *
     * @var string|null
     */
    protected $_breakOn = null;

    /**
     * The list of validation failures that occurred. Each element is the
     * name of a validator.
     *
     * @var array
     */
    protected $_failures = array();

    /**
     * Is the field valid according to the validation strategy?
     *
     * @var boolean
     */
    protected $_isValid = true;

    /**
     * Creates a Ruth_Channel_Field object.
     *
     * @param string $type The type of the field, must be a TYPE_ constant.
     * @return void
     */
    public function __construct($type = self::TYPE_STRING)
    {
        if ($type != self::TYPE_STRING and $type != self::TYPE_ARRAY) {
            throw new Ruth_Channel_Exception("invalid field type");
        }

        $this->_type = $type;

        switch ($this->_type) {
            case self::TYPE_STRING:
                $this->_value = '';
                break;
            case self::TYPE_ARRAY:
                $this->_value = array();
                break;
        }

        $this->_fields = new ArrayObject();
    }

    /**
     * Sets the list of fields to which this field belongs.
     *
     * @param ArrayObject $fields
     * @return Ruth_Channel_Field
     */
    public function setFields(ArrayObject $fields)
    {
        $this->_fields = $fields;
        return $this;
    }

    /**
     * Gets the list of fields to which this field belongs. 
     *
     * @return ArrayObject
     */
    public function getFields()
    {
        return $this->_fields;
    }

    /**
     * Sets the name of the field.
     *
     * @param string $name
     * @return Ruth_Channel_Field
     */
    public function setName($name)
    {
        $this->_name = $name;
        return $this;
    }

    /**
     * Gets the name of the field.
     * 
     * @return string
     */
    public function getName()
    {
        return $this->_name;
    }

    /**
     * Gets the name of the field, filter using the filter set by
     * setNameFilter. If no name filter is set, the name of the field is
     * returned unfiltered.
     *
     * @return string
     */
    public function getNameFiltered()
    {
        if (self::$_nameFilter !== null) {
            return self::$_nameFilter->filter($this->_name);
        } else {
            return $this->_name;
        }
    }

    /**
     * Sets the filter used by the getNameFiltered() method.
     *
     * @param Ruth_Filter_Abstract $filter
     */
    public static function setNameFilter(Ruth_Filter_Abstract $filter)
    {
        self::$_nameFilter = $filter;
    }

    /**
     * Gets the filter used by the getNameFiltered() method.
     *
     * @return Ruth_Filter_Abstract|null
     */
    public static function getNameFilter()
    {
        return self::$_nameFilter;
    }

    /**
     * Assigns the value of the field, casting it to the type set when
     * constructing the field object.
     *
     * @param mixed $value
     * @return Ruth_Channel_Field
     */
    public function setValue($value)
    {
        $this->_value = $value;

        switch ($this->_type) {
            case self::TYPE_STRING:
                $this->_value = (string) $this->_value;
                break;
            case self::TYPE_ARRAY:
                $this->_value = (array) $this->_value;
                break;
        }

        return $this;
    }

    /**
     * Returns the value of the field. 
     *
     * @return string|array
     */
    public function getValue()
    {
        return $this->_value;
    }

    /**
     * Returns the value of the field as a string.
     *
     * @return string
     */
    public function __toString()
    {
        try {
            return strval($this->getValue());
        } catch (Exception $e) {
            // __toString is not allowed to throw an exception
            return '';
        }
    }

    /**
     * Adds a filter to the field.  If $param is a Ruth_Filter_Abstract
     * object, it is added to the field and the other two parameters are
     * ignored. If $param is a string, it and the other two parameters are
     * used to instantiate a filter which is then added to the field.
     *
     * @param string|Ruth_Filter_Abstract $param
     * @param array $options
     * @param string|null $name
     * @return Ruth_Channel_Field
     */
    public function filter($param, array $options=array())
    {
        if ($param instanceof Ruth_Filter_Abstract) {
            $filter = $param;
        } else {
            $filter = Ruth_Filter::instantiate($param, $options);
        }

        $this->_filters[] = $filter;
        return $this;
    }

    /**
     * Adds a validator to the field.  If $param is a Ruth_Validate_Abstract
     * object, it is added to the field and the other two parameters are
     * ignored. If $param is a string, it and the other two parameters are
     * used to instantiate a validator which is then added to the field.
     *
     * @param string|Ruth_Validate_Abstract $param
     * @param array $options
     * @param string|null $name
     * @return Ruth_Channel_Field
     */
    public function is($param, array $options=array(), $name=null)
    {
        if ($param instanceof Ruth_Validate_Abstract) {
            $validator = $param;
            $name = $validator->getName();
        } else {
            $validator = Ruth_Validate::instantiate($param, $options, $name);
            $name = $validator->getName();
        }

        $this->_validators[$name] = $validator;
        return $this;
    }

    /**
     * Adds a validation "hook", allowing some sort of external validation to
     * be done, the result of which can be assigned to this field and
     * validator via the setFailure() method.
     *
     * @param string $name
     * @return Ruth_Channel_Field
     */
    public function validationHook($name)
    {
        $validator = Ruth_Validate::instantiate('Anything', array(), $name);
        $name = $validator->getName();
        $this->_validators[$name] = $validator; 

        return $this;
    }

    /**
     * If the validator named $name fails, then no subsequent validators
     * in the field are run against the value.  Any validators not run will
     * be left in a passing state.
     *
     * @param string|null $name
     * @return Ruth_Channel_Field
     */
    public function breakOn($name)
    {
        $this->_breakOn = $name;
        return $this;
    }

    /**
     * Sets the validation strategy for the field. The validation strategy
     * determines the conditions under which a field is considered valid
     * overall.  For example if $strategy is STRATEGY_ANY, then the field
     * is considered value if any of its validators pass.
     *
     * @param string $strategy Must be one of the STRATEGY_* constants.
     * @return Ruth_Channel_Field
     */
    public function validationStrategy($strategy)
    {
        $this->_validationStrategy = $strategy;
        $this->_isValid = $this->_determineIsValid();
        return $this;
    }

    /**
     * Removes all filters of type $type.
     *
     * @param string $type
     * @return Ruth_Channel_Field
     */
    public function removeFiltersByType($type)
    {
        $filters = array();

        foreach ($this->_filters as $filter) {
            if (!($filter instanceof $type)) {
                $filters[] = $filter;
            }
        }

        $this->_filters = $filters;

        return $this;
    }

    /**
     * Returns the validator with name $name or throws an exception if the
     * validator is not set.
     *
     * @param string $name
     * @return Ruth_Validator_Abstract
     * @throws Ruth_Channel_Exception
     */
    public function getValidator($name)
    {
        if (isset($this->_validators[$name])) {
            return $this->_validators[$name];
        } else {
            throw new Ruth_Channel_Exception("validator with name {$name} not registered");
        }
    }

    /**
     * Removes all validators of type $type.
     *
     * @param string $type
     * @return Ruth_Channel_Field
     */
    public function removeValidatorsByType($type)
    {
        $validators = array();

        foreach ($this->_validators as $validator) {
            if (!($validator instanceof $type)) {
                $validators[] = $validator;
            } else {
                unset($this->_failures[$validator->getName()]);
                $this->_isValid = $this->_determineIsValid();
            }
        }

        $this->_validators = $validators;

        return $this;
    }

    /**
     * Removes the validator with name $name, or does nothing if the specified
     * validator is not set.
     *
     * @param string $name
     * @return Ruth_Channel_Field
     */
    public function removeValidatorByName($name)
    {
        unset($this->_validators[$name]);
        unset($this->_failures[$name]);

        $this->_isValid = $this->_determineIsValid();
        
        return $this;
    }

    /**
     * Returns true if the validator specified by $name validated, false
     * otherwise.  An exception is thrown if $name is not a registered
     * validator.
     *
     * @param string $name
     * @return boolean
     * @throws Ruth_Channel_Exception
     */
    public function validates($name=null)
    {
        if (is_null($name)) {
            return $this->_isValid;
        } else if (isset($this->_validators[$name])) {
            return !isset($this->_failures[$name]);
        } else {
            throw new Ruth_Channel_Exception("validator with name {$name} not set");
        }
    }

    /**
     * Marks a validator failure on this field, regardless of whether or not 
     * the validator failed or runValidators() has been called.
     *
     * @param string $name
     * @return Ruth_Channel_Field
     */
    public function setFailure($name)
    {
        if (!isset($this->_validators[$name])) {
            throw new Ruth_Channel_Exception("validator with name {$name} not set");
        }

        $this->_failures[$name] = true;
        $this->_isValid = $this->_determineIsValid();
        return $this;
    }

    /**
     * Resets the field so that another value can be filtered/validated.
     *
     * @return Ruth_Channel_Field
     */
    public function reset()
    {
        $this->_failures = array();
        $this->_isValid = true;

        switch ($this->_type) {
            case self::TYPE_STRING:
                $this->_value = '';
                break;
            case self::TYPE_ARRAY:
                $this->_value = array();
                break;
        }

        return $this;
    }

    /**
     * Checks if a particular validator passed or failed, or if the field
     * should be considered valid overall.
     *
     * Prefix the name of the validator with either is or matches. For example,
     * with a validator named Empty, $field->isEmpty returns true or 
     * false based upon whether the validator Empty passed or failed.
     *
     * Use isValid to check the overall validity of the field.
     *
     * @param string $name
     * @return boolean
     */
    public function __get($name)
    {
        if (preg_match('/^(is|matches)(.*)/', $name, $m)) {
            $validateName = $m[2];

            if ($validateName == 'Valid') {
                return $this->validates();
            } else {
                return $this->validates($validateName);
            }
        } else {
            return $this->getValidator($name);
        }
    }

    /**
     * Runs the registered filters over the field value, modifying the value
     * of the field.
     *
     * @return Ruth_Channel_Field
     */
    public function runFilters()
    {
        foreach ($this->_filters as $filter) {
            $filterMethod = new Ruth_Annotation_ReflectionMethod(
                $filter, 'filter');

            if ($filterMethod->hasAnnotation('param')) {
                $paramType = $filterMethod->getAnnotation('param');
            } else {
                $paramType = 'string';
            }

            if ($paramType == 'string' and is_array($this->_value)) {
                self::_filterRecursive($filter, $this->_value);
            } else {
                $this->_value = $filter->filter($this->_value);
            }
        }

        return $this;
    }

    /**
     * Runs the registered validators over the field value.
     *
     * @return Ruth_Channel_Field
     */
    public function runValidators()
    {
        foreach ($this->_validators as $validator) {
            if ($validator instanceof Ruth_Channel_Validate_Omni) {
                $validator->setFields($this->_fields);
            }

            $isValidMethod = new Ruth_Annotation_ReflectionMethod(
                $validator, 'isValid');

            if ($isValidMethod->hasAnnotation('param')) {
                $paramType = $isValidMethod->getAnnotation('param');
            } else {
                $paramType = 'string';
            }

            if ($paramType == 'string' and is_array($this->_value)) {
                if (!self::_validateRecursive($validator, $this->_value)) {
                    $this->_failures[$validator->getName()] = true;
                }
            } else {
                if (!$validator->isValid($this->_value)) {
                    $this->_failures[$validator->getName()] = true;
                }
            }

            if (!is_null($this->_breakOn) and isset($this->_failures[$this->_breakOn])) {
                $this->_isValid = $this->_determineIsValid();
                return;
            }
        }

        $this->_isValid = $this->_determineIsValid();
        return $this;
    }

    /**
     * Recursively applies a filter to an array of values.
     *
     * @param Ruth_Filter_Abstract $filter
     * @param array $values
     */
    protected static function _filterRecursive(Ruth_Filter_Abstract $filter, array &$values)
    {
        foreach ($values as &$value) {
            if (is_array($value)) {
                self::_filterRecursive($filter, $value);
            } else {
                $value = $filter->filter($value);
            }
        }
    }

    /**
     * Recursively checks values of an array for validity using a validator. 
     *
     * @param Ruth_Validate_Abstract $validate
     * @param array $values
     * @return boolean
     */
    protected static function _validateRecursive(Ruth_Validate_Abstract $validate, array &$values)
    {
        foreach ($values as &$value) {
            if (is_array($value)) {
                if (!self::_validateRecursive($validate, $value)) {
                    return false;
                }
            } else {
                if (!$validate->isValid($value)) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Determines if the field should be considered valid based upon
     * the current validation failures and validation strategy.
     * 
     * @return boolean
     */
    private function _determineIsValid()
    {
        if ($this->_validationStrategy == self::STRATEGY_ANY) {
            return ((count($this->_validators) - count($this->_failures)) > 0)
                or count($this->_validators) == 0;
        } else {
            return count($this->_failures) == 0;
        }
    }
}
 
// vim: sw=4:ts=4:sts=4:et
